<html>
 <head>
  <meta charset="UTF-8">
 </head>
 <body>
  <h2 data-lake-id="jA7GJ" id="jA7GJ"><span data-lake-id="u684fe80b" id="u684fe80b">Redis过期消息实现延迟消息</span></h2>
  <p data-lake-id="uc0554639" id="uc0554639"><span data-lake-id="u0ba22726" id="u0ba22726">很多用过Redis的人都知道，Redis有一个过期监听的功能，</span></p>
  <p data-lake-id="u5a3e33bf" id="u5a3e33bf"><span data-lake-id="u893f0689" id="u893f0689">​</span><br></p>
  <p data-lake-id="u6b8790e7" id="u6b8790e7"><span data-lake-id="u29b5973d" id="u29b5973d">在 redis.conf 中，加入一条配置</span><span data-lake-id="ubedb75ea" id="ubedb75ea">notify-keyspace-events Ex</span><span data-lake-id="u90a30067" id="u90a30067">开启过期监听，然后再代码中实现一个KeyExpirationEventMessageListener，就可以监听key的过期消息了。</span></p>
  <p data-lake-id="uaf877970" id="uaf877970"><span data-lake-id="u26a7cabe" id="u26a7cabe">​</span><br></p>
  <p data-lake-id="uea14d485" id="uea14d485"><span data-lake-id="uc06b9123" id="uc06b9123">这样就可以在接收到过期消息的时候，进行订单的关单操作。</span></p>
  <p data-lake-id="u414989fe" id="u414989fe"><span data-lake-id="uf3ec79ea" id="uf3ec79ea">​</span><br></p>
  <p data-lake-id="u869b72f1" id="u869b72f1"><span data-lake-id="ud5633bc5" id="ud5633bc5">这个方案不建议大家使用，是因为Redis官网上明确的说过，Redis并不保证Key在过期的时候就能被立即删除，更不保证这个消息能被立即发出。所以，消息延迟是必然存在的，随着数据量越大延迟越长，延迟个几分钟都是常事儿。</span></p>
  <p data-lake-id="u181dc388" id="u181dc388"><span data-lake-id="u8b1e4a21" id="u8b1e4a21">​</span><br></p>
  <p data-lake-id="ue1a136af" id="ue1a136af"><span data-lake-id="u8946ce05" id="u8946ce05">而且，在Redis 5.0之前，这个消息是通过PUB/SUB模式发出的，他不会做持久化，至于你有没有接到，有没有消费成功，他不管。也就是说，如果发消息的时候，你的客户端挂了，之后再恢复的话，这个消息你就彻底丢失了。</span></p>
  <h2 data-lake-id="uPSBq" id="uPSBq"><span data-lake-id="ua7fc3e85" id="ua7fc3e85">Redis的zset实现延迟消息</span></h2>
  <p data-lake-id="uf664f561" id="uf664f561"><span data-lake-id="ua8da94aa" id="ua8da94aa">虽然基于Redis过期监听的方案并不完美，但是并不是Redis实现关单功能就不完美了，还有其他的方案。</span></p>
  <p data-lake-id="u519286d1" id="u519286d1"><span data-lake-id="udb3a683a" id="udb3a683a">​</span><br></p>
  <p data-lake-id="u7fbc5405" id="u7fbc5405"><span data-lake-id="u69851f11" id="u69851f11">我们可以借助Redis中的有序集合——zset来实现这个功能。</span></p>
  <p data-lake-id="u6b124e64" id="u6b124e64"><span data-lake-id="u25a11c53" id="u25a11c53">​</span><br></p>
  <p data-lake-id="u14cd5001" id="u14cd5001"><span data-lake-id="u257a3631" id="u257a3631">zset是一个有序集合，每一个元素(member)都关联了一个 score，可以通过 score 排序来取集合中的值。</span></p>
  <p data-lake-id="u88bda1f7" id="u88bda1f7"><span data-lake-id="u5fdb1889" id="u5fdb1889">​</span><br></p>
  <p data-lake-id="u25a92a3e" id="u25a92a3e"><span data-lake-id="u2c3fc797" id="u2c3fc797">我们将订单超时时间的时间戳（下单时间+超时时长）与订单号分别设置为 score 和 member。这样redis会对zset按照score延时时间进行排序。然后我们再开启redis扫描任务，获取”当前时间 &gt; score”的延时任务，扫描到之后取出订单号，然后查询到订单进行关单操作即可。</span></p>
  <p data-lake-id="u21cc47a6" id="u21cc47a6"><span data-lake-id="u7981c331" id="u7981c331">​</span><br></p>
  <p data-lake-id="u773b6fb7" id="u773b6fb7"><span data-lake-id="u8f2f2b1d" id="u8f2f2b1d">使用redis zset来实现订单关闭的功能的优点是可以借助redis的持久化、高可用机制。避免数据丢失。但是这个方案也有缺点，那就是在高并发场景中，有可能有多个消费者同时获取到同一个订单号，一般采用加分布式锁解决，但是这样做也会降低吞吐型。</span></p>
  <p data-lake-id="ubab8dd8b" id="ubab8dd8b"><span data-lake-id="u693b24be" id="u693b24be">​</span><br></p>
  <p data-lake-id="uedc563f7" id="uedc563f7"><span data-lake-id="u1ef32d7e" id="u1ef32d7e">但是，在大多数业务场景下，如果幂等性做得好的，多个消费者取到同一个订单号也无妨。</span></p>
  <h2 data-lake-id="H8JMe" id="H8JMe"><span data-lake-id="u822c2f2c" id="u822c2f2c">Redission实现延迟消息</span></h2>
  <p data-lake-id="u8b1355f6" id="u8b1355f6"><span data-lake-id="u736f88fd" id="u736f88fd">上面这种方案看上去还不错，但是需要我们自己基于zset这种数据结构编写代码，那么有没有什么更加友好的方式？</span></p>
  <p data-lake-id="uc95b7d0f" id="uc95b7d0f"><span data-lake-id="u61081762" id="u61081762">​</span><br></p>
  <p data-lake-id="u00e1b38c" id="u00e1b38c"><span data-lake-id="ub62daaf4" id="ub62daaf4">有的，那就是基于Redisson。</span></p>
  <p data-lake-id="u45ad2c7d" id="u45ad2c7d"><span data-lake-id="uf14819ee" id="uf14819ee">​</span><br></p>
  <p data-lake-id="u74c0bbb1" id="u74c0bbb1"><span data-lake-id="u16f8b29a" id="u16f8b29a">Redisson是一个在Redis的基础上实现的框架，它不仅提供了一系列的分布式的Java常用对象，还提供了许多分布式服务。</span></p>
  <p data-lake-id="u1fbfde69" id="u1fbfde69"><br></p>
  <p data-lake-id="u9db06c56" id="u9db06c56"><span data-lake-id="ue2f7df2f" id="ue2f7df2f">Redission中定义了分布式延迟队列RDelayedQueue，这是一种基于我们前面介绍过的zset结构实现的延时队列，它允许以指定的延迟时长将元素放到目标队列中。</span></p>
  <p data-lake-id="u7a9862f7" id="u7a9862f7"><span data-lake-id="ue176b6c3" id="ue176b6c3">​</span><br></p>
  <p data-lake-id="ud46e4271" id="ud46e4271"><span data-lake-id="u218221b8" id="u218221b8">其实就是在zset的基础上增加了一个基于内存的延迟队列。当我们要添加一个数据到延迟队列的时候，redission会把数据+超时时间放到zset中，并且起一个延时任务，当任务到期的时候，再去zset中把数据取出来，返回给客户端使用。</span></p>
  <p data-lake-id="uc1810a60" id="uc1810a60"><span data-lake-id="ud1983ca9" id="ud1983ca9">​</span><br></p>
  <p data-lake-id="u097885eb" id="u097885eb"><span data-lake-id="uff4b4ace" id="uff4b4ace">大致思路就是这样的，感兴趣的大家可以看一看RDelayedQueue的具体实现。</span></p>
  <p data-lake-id="u34d48cbe" id="u34d48cbe"><span data-lake-id="ud5f9b7f4" id="ud5f9b7f4">​</span><br></p>
  <p data-lake-id="ub9e60b54" id="ub9e60b54"><span data-lake-id="u53430101" id="u53430101">基于Redisson的实现方式，是可以解决基于zset方案中的并发重复问题的，而且还能实现方式也比较简单，稳定性、性能都比较高。</span></p>
  <p data-lake-id="ub06e7b34" id="ub06e7b34"><br></p>
 </body>
</html>